Skip to content

fix(react): extend the adapter-only data.* constant-predicate diagnostic to disabled / disabledOn - #6702

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6504-disabled-gate-constant-predicate
Aug 28, 2026
Merged

fix(react): extend the adapter-only data.* constant-predicate diagnostic to disabled / disabledOn#6702
os-sales merged 1 commit into
mainfrom
claude/issue-6504-disabled-gate-constant-predicate

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes #6504

What

Extends objectui#5687's adapter-only data.* constant-predicate diagnostic to the
disabled / disabledOn gate — dev-only, per the 2026-08-27 maintainer ruling (Option A)
recorded on this issue.

reportAdapterOnlyDataPredicate names a predicate that evaluates perfectly, against the
wrong object: at the node tier data is the data-source adapter, not the row, so
data.status == 'locked' is a constant for every row. It was wired only into the
visibility chain's dev leg (evaluateVisibilityPredicate). A disabled predicate written
the same way faults nothing (#6445's fault reporter correctly stays silent), and on the
constant's other polarity (data.locked == null, !data.assignee, or an adapter answering
nothing) hands the gate a constant true that greys the control out on every row, in every
build, with nothing on the console. evaluateEnablementPredicate now reports it too, in
development only — option C (always-on) stays excluded, outside the #5687 precedent.

The copy, and why its direction differs from the visibility leg

The visibility leg's sentence — "a constant false hides the node on every row while
looking exactly like a gate that said no"
— is about the wrong polarity for this gate and
would be a false statement here. The ruling named the direction explicitly: on disabled,
the dangerous constant is true. The new enablement entry in ADAPTER_ONLY_GATE_COPY
(packages/react/src/utils/visibilityDiagnostic.ts) reads:

...and on THIS gate the dangerous polarity is a constant true: the node renders
DISABLED - on screen, greyed out, refusing input - on every row, in every build, while
looking exactly like a gate that said no. No pixel says the predicate is a constant, so
this line is the only thing that will ever name it.

It is not a reuse of the visibility sentence — the two are pinned as distinct strings, and
one new group-4-style test in SchemaRenderer.disabledGateFaultDiagnostic.test.tsx asserts
the enablement line does not contain hides the and the visibility line does not contain
DISABLED.

Where the dissolution pointer lives, and its verbatim source

Both #5687's own docblock and the new binding-inheritance clause are carried forward:
unresolvedDataPaths's "why this discriminator" section already reads (verbatim, unchanged
by this PR): "a diagnostic on a card that dissolves when objectui#5330's deprecation
window closes."
This PR appends a clause at that exact spot — "BOTH legs together, the
enablement one objectui#6504 added included"
— and adds a new docblock on
ADAPTER_ONLY_GATE_COPY itself that quotes the 2026-08-27 ruling's binding clause verbatim
and names every symbol + both call sites (evaluateVisibilityPredicate and
evaluateEnablementPredicate in SchemaRenderer.tsx) a #5330-window teardown has to delete
in the same stroke. Neither leg is independently scheduled.

Per the stop condition: #5330's deprecation window is not closing near-term — measured,
not assumed. Its Phase 2 carrier (#5741, the stored-metadata survey that gates removal) is
still open, pm:awaiting-maintainer, unassigned, blocked on a live-tenant data channel no
seat currently has. Proceeding per the ruling.

Ablation — both legs predicted before running, mutation/restoration proven on disk

Leg 1 — the new leg must produce the diagnostic, with the correct copy (evaluateEnablementPredicate's new call site removed).
Predicted: every new group-6 test that asserts a report turns red; every silence/negative
control stays green. Observed: exactly as predicted — 8/8 red (the acceptance
criterion, the constant-true direction, the copy-is-not-reused assertion, the
cross-gate content check, disabledOn, the two dedupe cases, and the pre-existing test this
PR rewrote from a negative to a positive pin), 24/24 other cases (including every
false-positive/negative control) stayed green. Mutation confirmed on disk (call-site grep
count 1→0, blob hash changed); restore confirmed (git diff HEAD empty, hash back to
57c0beb3f…).

Leg 2 — false-positive direction (a genuinely row-dependent predicate must not fire it).
Mutated the shared discriminator regex (DATA_ROOT_PATH_RE, unchanged by this PR's actual
diff) to also treat record as a matched root, to probe whether the shared false-positive
protection this leg inherits (rather than reimplements) is real. Prediction missed, and
reported as observed rather than forced
: I predicted the new FALSE-POSITIVE CONTROL
test (disabled: "record.status == 'open'") and nodeGateDataPredicate.test.tsx's 3a
would flip; neither did — both stay silent behind an independent guard
(source.indexOf('data.') === -1 fast-reject: neither predicate string contains the
literal substring data. at all, so the mutated regex is never reached). What did flip
were two pre-existing, unrelated pins in nodeGateDataPredicate.test.tsx2d
(a quoted 'data.status' literal) and 3d (record.data.status, whose . before data
the original regex's exclusion group is specifically built to reject) — both already-hardened
false-positive guards this card didn't write, doing their job against a hostile version of
code this card also didn't write. Net: this leg adds no new discriminator logic (no new
regex, no new resolution algorithm), only a second call into the existing, shared,
already-tested function with a gate label — so its false-positive resistance is by
construction identical to the visibility leg's, not a new surface to separately harden.
Mutation confirmed on disk (marker count 0→1, hash changed); restore confirmed (git diff HEAD empty, hash back to 40e466e7d…).

Leg 3 — the visibility leg is unchanged. evaluateVisibilityPredicate's call site
(line ~772 in SchemaRenderer.tsx) is untouched — no gate argument added, still defaults
to 'visibility'. SchemaRenderer.nodeGateDataPredicate.test.tsx (40 tests, the full
#5687/#5756 suite) and SchemaRenderer.concealmentGateFaultDiagnostic.test.tsx pass
unmodified, byte-identical to origin/main. Leg 1's mutation (which only touched the
enablement call site) independently confirms zero effect on any visibility-leg test.

Gate table

Gate Command Result
Build dependency closure pnpm --filter '@object-ui/react^...' build types/core/data-objectstack/i18n all Done
Type-check pnpm --filter '@object-ui/react' run type-check (tsc --noEmit && tsc -p tsconfig.test.json) exit 0, no output (clean)
Vitest, disabledGateFaultDiagnostic.test.tsx pnpm exec vitest run packages/react/src/__tests__/SchemaRenderer.disabledGateFaultDiagnostic.test.tsx Test Files 1 passed (1) / Tests 32 passed (32)
Vitest, sibling diagnostics (unchanged) pnpm exec vitest run packages/react/src/__tests__/SchemaRenderer.nodeGateDataPredicate.test.tsx packages/react/src/__tests__/SchemaRenderer.concealmentGateFaultDiagnostic.test.tsx Test Files 2 passed (2) / Tests 40 passed (40)
Vitest, full package (root-level invocation, objectui#3378) pnpm exec vitest run packages/react/ Test Files 62 passed (62) / Tests 926 passed (926) — re-run after ablation restore, identical
check:control-bytes node scripts/check-control-bytes.mjs ✅ OK (scanned 5553 tracked text file(s); skipped 85 binary)
Changeset presence node scripts/check-changeset-presence.mjs ✅ 3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
Changeset no-major node scripts/check-changeset-no-major.mjs ✅ No changeset declares a major bump
Lint (narrowed, justified) pnpm exec eslint --no-inline-config --format json <3 touched files> 3 files linted (json count), 0 errors, 18 pre-existing no-explicit-any warnings on SchemaRenderer.tsx (none on touched lines — verified against the same file's content at origin/main's 9abc02ab2 via --stdin, also 0 errors/18 warnings, byte-identical count)

Lint narrowing justification (repo has no type-aware linting configured — no project /
projectService in eslint.config.js — so this diff cannot move any judgment on an
untouched file): file count is eslint's own --format json output (3), not a guess; the
config's rule set is untyped, so the invariance claim is structural, not assumed.

Union re-run: 20c52cedf (branch head, pushed) — the commit these results are quoted
against.

Fence

Touched only packages/react/src/utils/visibilityDiagnostic.ts,
packages/react/src/SchemaRenderer.tsx (the evaluateEnablementPredicate call site the
existing docblock there explicitly anticipated this card would fill in), and one test file.
Did not reach packages/types' exports map, app-shell, or plugin-designer — no
overlap with #6527's in-flight patch round.

No published type widened: AdapterOnlyPredicateGateKind and
ADAPTER_ONLY_ENABLEMENT_PREDICATE_PREFIX are module-internal exports (not re-exported from
packages/react/src/index.ts), matching #5687's own ADAPTER_ONLY_DATA_PREDICATE_PREFIX /
formatAdapterOnlyDataMessage / reportAdapterOnlyDataPredicate, none of which are on the
package's public barrel either. No runtime behaviour change outside __DEV__.


Generated by Claude Code

…ostic to `disabled` / `disabledOn`

`reportAdapterOnlyDataPredicate` (objectui#5687) reports a node-gate predicate that
evaluates perfectly, against the wrong object: at the node tier `data` is the
data-source adapter, not the row, so `data.status == 'locked'` is a constant for
every row. It was wired only into the visibility chain's dev leg.

The `disabled` / `disabledOn` gate was not covered: a predicate like
`data.status == 'locked'` evaluates cleanly (no fault, so objectui#6445's fault
reporter correctly stays silent), and on the constant's other polarity
(`data.locked == null`, `!data.assignee`, or an adapter answering nothing) hands
the gate a constant `true` that greys the control out on every row, in every
build, with nothing on the console.

Per the 2026-08-27 maintainer ruling (option A) on objectui#6504: extend the
diagnostic to the enablement gate, dev-only (option C, always-on, was excluded,
outside the #5687 precedent), with its own copy for the constant-`true`
direction rather than reusing the visibility leg's "hides the node" sentence.
Both legs carry the same objectui#5330 dissolution pointer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_8ca04858-ea8e-5b85-9182-de59aa49e00c
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 49 chunks) 3232.0 KB 3266.6 KB
Main entry chunk (gzip) 157.2 KB 350 KB
Entry file index-MtzM0a0A.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 509.32KB 115.60KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 239.05KB 60.06KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.89KB 12.91KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.26KB 34.43KB
plugin-designer (index.js) 212.80KB 43.15KB
plugin-detail (index.js) 245.29KB 62.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.01KB 32.23KB
plugin-gantt (index.js) 165.20KB 40.37KB
plugin-grid (index.js) 201.51KB 54.54KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 113.01KB 27.57KB
plugin-map (index.js) 20.17KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 26.44KB 7.59KB
plugin-tree (index.js) 8.91KB 3.05KB
plugin-view (index.js) 85.87KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 67.73KB 22.54KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The adapter-only data.* diagnostic (#5687) covers the visibility gate only — the same constant predicate on disabled greys a control out in silence

2 participants